from enum import Enum
from collections import namedtuple
import argparse
import subprocess
import sys
import os


class TargetType(Enum):
    Interface = 0

    Library = 1
    Executable = 2
    Cflags = 3
    Ldflags = 4


INTERFACE = TargetType.Interface

LIBRARY = TargetType.Library
EXECUTABLE = TargetType.Executable
CFLAGS = TargetType.Cflags
LDFLAGS = TargetType.Ldflags


_Feature = namedtuple("_Feature", ["name", "deps"])

_Target = namedtuple(
    "_Target",
    [
        "type",
        "name",
        "payload",
        "deps",
        "secret_deps",
    ],
)


# 在打开对应特性的时候依赖目标。
DepIf = namedtuple("DepIf", ["feature", "target"])


class DuplicatedNameError(Exception):
    def __init__(self, name):
        self.name = name


class UndefinedNameError(Exception):
    def __init__(self, name):
        self.name = name


def to_tuple(x, type):
    """将x转换为type类型的元组。"""
    if x is None or x == ():
        return ()

    if isinstance(x, type):
        return (x,)

    for xi in x:
        if not isinstance(xi, type):
            raise TypeError(xi, type)

    return tuple(x)


class Config:
    def __init__(self):
        self._targets = {}
        self._features = {}
        self._ninja_build = []
        self._builddir = ".build"

    def add_interface(self, name, deps=None, secret_deps=None):
        return self._add_target(INTERFACE, name, None, deps, secret_deps)

    def add_cflags(self, name, flags, deps=None, secret_deps=None):
        return self._add_target(
            CFLAGS,
            name,
            to_tuple(flags, str),
            deps,
            secret_deps,
        )

    def add_ldflags(self, name, flags, deps=None, secret_deps=None):
        return self._add_target(
            LDFLAGS,
            name,
            to_tuple(flags, str),
            deps,
            secret_deps,
        )

    def add_library(self, name, srcs, deps=None, secret_deps=None):
        return self._add_target(
            LIBRARY,
            name,
            to_tuple(srcs, str),
            deps,
            secret_deps,
        )

    def add_executable(self, name, srcs, deps=None, secret_deps=None):
        return self._add_target(
            EXECUTABLE,
            name,
            to_tuple(srcs, str),
            deps,
            secret_deps,
        )

    def _add_target(self, type, name, payload, deps=None, secret_deps=None):
        if name in self._targets:
            raise DuplicatedNameError(name)

        target = _Target(
            type=type,
            name=name,
            payload=payload,
            deps=to_tuple(deps, (DepIf, _Target)),
            secret_deps=to_tuple(secret_deps, (DepIf, _Target)),
        )
        self._targets[name] = target
        return target

    def add_feature(self, name, deps=None):
        if name in self._features:
            raise DuplicatedNameError(name)

        feature = _Feature(name, deps=to_tuple(deps, _Feature))
        self._features[name] = feature

        return feature

    def _resolve_features_rec(self, enable, enable_all, is_top=False):
        if not isinstance(enable, _Feature):
            raise TypeError(enable, _Feature)

        if not isinstance(enable_all, set):
            raise TypeError(enable, set)

        if enable in enable_all:
            return

        feature = self._features.get(enable.name)
        if feature is None:
            raise UndefinedNameError(enable)

        enable_all.add(feature)
        for dep in feature.deps:
            self._resolve_features_rec(dep, enable_all)

    def _resolve_features(self, feature_names):
        if "default" not in self._features:
            self._features["default"] = _Feature("default", deps=())

        features = []
        for each in feature_names:
            if each not in self._features:
                raise NameError(each)
            features.append(self._features[each])

        features_all = set()
        for each in features:
            self._resolve_features_rec(each, features_all, is_top=True)

        return features_all

    def _collect_deps(self, target, deps_set, features, is_top=False):
        direct_deps = target.deps
        if is_top:
            direct_deps += target.secret_deps

        for direct_dep in direct_deps:
            if isinstance(direct_dep, DepIf):
                if direct_dep.feature in features:
                    direct_dep = direct_dep.target
                else:
                    continue

            if not deps_set.add(direct_dep):
                continue
            self._collect_deps(direct_dep, deps_set, features)

    def _load_target_deps(self, target, features):
        deps_set = OrderedSet()
        self._collect_deps(target, deps_set, features, is_top=True)
        return deps_set.get_list()

    def _nprint(self, fmt, *args, **kwargs):
        self._ninja_build.append(fmt.format(*args, **kwargs))

    def _ninja_header(self):
        self._nprint("cc = gcc")
        self._nprint("ar = ar")
        self._nprint("cflags =")
        self._nprint("ldflags =")
        self._nprint("builddir = {}", self._builddir)
        self._nprint("objdir = $builddir/obj")
        self._nprint("bindir = $builddir/bin")
        self._nprint("libdir = $builddir/lib")

        self._nprint("rule cc")
        self._nprint("  command = $cc -MD -MF $out.d $cflags -o $out -c $in")
        self._nprint("  depfile = $out.d")
        self._nprint("rule ar")
        self._nprint("  command = $ar rs $out $in")
        self._nprint("rule link")
        self._nprint("  command = $cc $ldflags -o $out $in")

    def build(self):
        ap = argparse.ArgumentParser()
        ap.add_argument(
            "-f",
            "--feature",
            nargs="*",
            default=["default"],
            help="Enable build features",
        )

        opt = ap.parse_args()
        logging("open features: {}", opt.feature)

        features = self._resolve_features(opt.feature)
        logging("resolved open features: {}", tuple(f.name for f in features))

        self._ninja_header()

        for name in sorted(self._targets.keys()):
            target = self._targets[name]

            if target.type == LIBRARY or target.type == EXECUTABLE:
                self._build_target(
                    target,
                    features,
                    is_executable=target.type == EXECUTABLE,
                )

        if not os.path.isdir(self._builddir):
            os.makedirs(self._builddir)

        ninjafile = os.path.join(self._builddir, "build.ninja")
        with open(ninjafile, "w") as writer:
            for line in self._ninja_build:
                writer.write(line + "\n")

        ninja = subprocess.Popen(
            ["ninja", "-f", ninjafile],
            stdin=subprocess.DEVNULL,
        )
        code = ninja.wait()

        if code != 0:
            logging("ninja report error: {}", code)
            exit(1)

    def _collect_cflags(self, deps):
        cflags = OrderedSet()
        for dep in deps:
            if dep.type == CFLAGS:
                cflags.extend(dep.payload)
        return cflags.get_list()

    def _collect_ldflags(self, deps):
        ldflags = OrderedSet()
        for dep in deps:
            if dep.type == LDFLAGS:
                ldflags.extend(dep.payload)
        return ldflags.get_list()

    def _collect_link_libs(self, deps):
        libs = OrderedSet()
        for dep in deps:
            if dep.type == LIBRARY:
                libs.add(dep.name)
        return libs.get_list()

    def _build_target(self, target, features, is_executable=False):
        deps = self._load_target_deps(target, features)

        cflags = self._collect_cflags(deps)
        ldflags = self._collect_ldflags(deps)

        link_libs = self._collect_link_libs(deps)
        link_lib_paths = list(f"$libdir/lib{name}.a" for name in link_libs)

        objs = []
        for src in target.payload:
            obj = f"$objdir/{src}.o"

            self._nprint("build {obj}: cc {src}", obj=obj, src=src)
            if len(cflags) != 0:
                self._nprint("  cflags = {}", " ".join(cflags))

            objs.append(obj)

        if len(deps) != 0:
            logging(
                "{}: depends on {}",
                target.name,
                tuple(dep.name for dep in deps),
            )

        components = objs + link_lib_paths

        if is_executable:
            self._nprint(
                "build $bindir/{name}: link {components}",
                name=target.name,
                components=" ".join(components),
            )
            if len(ldflags) != 0:
                self._nprint("  ldflags = {}", " ".join(ldflags))
        else:
            self._nprint(
                "build $libdir/lib{name}.a: ar {components}",
                name=target.name,
                components=" ".join(components),
            )


def logging(fmt, *args):
    print("[jiamake]", fmt.format(*args), file=sys.stderr)


class OrderedSet:
    def __init__(self):
        self._set = set()
        self._list = []

    def add(self, x):
        if x in self._set:
            return False

        self._set.add(x)
        self._list.append(x)
        return True

    def extend(self, xs):
        for x in xs:
            self.add(x)

    def get_list(self):
        return self._list
